package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisData;
import io.netty.handler.codec.json.JsonObjectDecoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Autowired(required = false)
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;

    /**
     * 基于redis查询商铺
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id) {
        //缓存穿透
        //Shop shop = queryWithPassThrough(id);
//        Shop shop= cacheClient
//                .queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);

        //互斥锁解决缓存击穿
        //        Shop shop = queryWithMutex(id);

        //逻辑过期解决缓存击穿
        //Shop shop = queryWithLogicalExpire(id);
        Shop shop = cacheClient.queryWithLogicalExpire(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);

        if(shop==null)
            return Result.fail("店铺不存在!");

        return Result.ok(shop);
    }

    /**
     * 互斥锁解决缓存击穿
     * @param id
     * @return
     */
    public Shop queryWithMutex(Long id) {
        //1.从Redis内查询商品缓存
        String key=CACHE_SHOP_KEY + id;
        Map<Object, Object>  shopMap=stringRedisTemplate.opsForHash().entries(key);
        //2，redis中不为空,也不为我们存入的”null“值
        String s=String.valueOf(shopMap.get("a"));
        if(shopMap.size()!=0&& !s.equals( "a")){
            //转为空shop对象
            Shop shop =BeanUtil.fillBeanWithMap(shopMap,new Shop(), false);;
            return shop;
        }
        //3，如果数据为"a"和"a" ，说明数据库无数据，直接返回，不用查询数据库
        if ( s.equals( "a")){
            return null;
        }
        //4,开始实现缓存重建
        //4.1 获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        boolean isLock = tryLock(lockKey);
        Shop shop =null;
        try {
               //4.2 判断是否获取成功
               if (!isLock){
                  //4.3，失败，则休眠并重试
                   Thread.sleep(50);
                   //进行递归循环
                   return queryWithMutex(id);
               }
               //5，不存在就根据id查询数据库
               shop = getById(id);
               //模拟重建的延时
               Thread.sleep(200);
               //6，数据库也为空
               if(shop==null){
                   //将特殊map写入redis
                          stringRedisTemplate.opsForHash().putAll(key,shopMap);
                   //设置失效时间
                   stringRedisTemplate.expire(key,CACHE_NULL_TTL, TimeUnit.MINUTES);
                   return null;
               }
               //7，数据库不为空，数据写入Redis
               //转为Hashmap存入redis
               Map<String, Object>  shopMap2=BeanUtil.beanToMap(
                       shop,
                       new HashMap<>(),                      //第二个参数为map
                       CopyOptions.create()                  //创建CopyOptions对象
                               .setIgnoreNullValue(true)   //忽略空值
                              .setFieldValueEditor(           //对字段进行操作，使用lambda表达式
                                       (fieldName,filedValue)   ->  filedValue.toString())
               ) ;
               stringRedisTemplate.opsForHash().putAll(key,shopMap2);
               //8，设置失效时间
               stringRedisTemplate.expire(key,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //9，释放互斥锁
            unLock(lockKey);
        }
        return shop;
    }

    /**互斥锁实现解决缓存穿透**/
    public Shop queryWithPassThrough(Long id){
        //1.从Redis内查询商品缓存
        String key=CACHE_SHOP_KEY + id;
        Map<Object, Object>  shopMap=stringRedisTemplate.opsForHash().entries(key);
        //redis中不为空,也不为我们存入的”null“值
        String s=String.valueOf(shopMap.get("a"));
        if(shopMap.size()!=0&& !s.equals( "a")){
            //转为空shop对象
            Shop shop =BeanUtil.fillBeanWithMap(shopMap,new Shop(), false);;
            return shop;
        }
        //如果数据为"a"和"a" ，说明数据库无数据，直接返回，不用查询数据库
        if ( s.equals( "a")){
            return null;
        }
        //不存在就根据id查询数据库
        Shop shop = getById(id);
        //数据库也为空
        if(shop==null){
            //将特殊map写入redis
            shopMap.put("a","a");
            stringRedisTemplate.opsForHash().putAll(key,shopMap);
            //设置失效时间
            stringRedisTemplate.expire(key,CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //数据库不为空，数据写入Redis
        //转为Hashmap存入redis
        Map<String, Object>  shopMap2=BeanUtil.beanToMap(
                shop,
                new HashMap<>(),                      //第二个参数为map
                CopyOptions.create()                  //创建CopyOptions对象
                        .setIgnoreNullValue(true)   //忽略空值
                        .setFieldValueEditor(           //对字段进行操作，使用lambda表达式
                                (fieldName,filedValue)   ->  filedValue.toString())
        ) ;
        stringRedisTemplate.opsForHash().putAll(key,shopMap2);
        //设置失效时间
        stringRedisTemplate.expire(key,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return shop;
    }
    /**
     * 获取互斥锁
     */
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);  //需要将flag转成基本类型，并且为了避免自动 拆箱（拆箱有可能出现null值）使用工具类
                                                               //isTrue:只有当flag是true才是true，flag为false和null都返回false
    }

    /**
     * 释放互斥锁
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    //开启10个线程
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    /**
     * 逻辑过期实现解决缓存击穿
     **/
    public Shop queryWithLogicalExpire(Long id){
        //1.从Redis内查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断是否存在
        if(StrUtil.isBlank(shopJson)){
            return null;
        }
        //3.命中，需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
        JSONObject data=(JSONObject) redisData.getData();
        Shop shop=JSONUtil.toBean(data,Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        //3，命中则判断是否过期，未过期则返回老数据shop
        if(expireTime.isAfter(LocalDateTime.now())){
            return shop;
        }
         //4，过期则需要缓存重建
         //5，尝试获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
         //6，判断是否获取锁
         boolean isLock = tryLock(lockKey);
         if (isLock){
             //6.3,成功。开启独立线程，实现缓存重建
             CACHE_REBUILD_EXECUTOR.submit(
                     ( )->{
                       //重建缓存
                         try {
                             saveShop2Redis(id,20L);
                         } catch (InterruptedException e) {
                             throw new RuntimeException(e);
                         }finally {
                             //释放锁
                             unLock(lockKey);
                         }
                     });
         }
        return shop;
    }
    /**逻辑过期缓存重建方法**/
    public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
        //1.查询店铺信息
        Shop shop = getById(id);
        Thread.sleep(200);
        //2.封装逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        //3.写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        if(shop.getId()==null){
            return Result.fail("店铺id不能为空!");
        }
        //1.更新数据库
        updateById(shop);
        //2.删除缓存
        String key = CACHE_SHOP_KEY + shop.getId();
        stringRedisTemplate.delete(key);
        return Result.ok();
    }

}
